#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
-------------------------------------------------
   File Name：    page_element.py
   Author :       zyq
   CreateDate：   2024/7/12 11:11
   Remark :  页面对象底层框架优化
-------------------------------------------------
"""
import time

from selenium.webdriver.common.alert import Alert
from selenium.webdriver.common.by import By
from selenium.webdriver.remote.webelement import WebElement
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.support.select import Select
from selenium.webdriver.support import expected_conditions as ec
from selenium.webdriver.support.wait import WebDriverWait

from guitesting.common.util.logs import Log
from guitesting.common.util.image_captcha_recognizer import image_identify
from guitesting.common.bash.bash_ui.base_page import BasePage
from guitesting.common.util.return_time import ReturnTime

"""
我的封装思想：
数据描述符中可以使用到instance这个参数，他是被描述类的实例，也就是说它是页面对象。并且它继承了元素查找类（basePage）。
描述的属性就是页面元素，也就是页面对象中的元素。
通过元素属性来使用交互操作。
在交互操作中需要使用元素查找类的方法时，通过instance来调用。

为什么要使用instance来调用？
因为页面对象只实例化一次，basePage初始化也一次，driver就只获取一次。尽可能接耦合。

为什么不全部写在一次？
当然可以写在一起，但是这样写的话，每次描述元素属性时，都会执行一次初始化，这不是我想看到的。
并且在使用描述属性时，会检索出查找元素的方法，我希望都是一些操作类的方法，而不是和查找元素的方法一起被检索。

为什么使用描述符来进行封装？
页面对象是把一个页面的所有东西封装层一个对象，这个对象中封装了页面元素，以及元素的操作。
那么页面元素是不是也可以是个对象？我通过这个页面元素对象来调用交互操作，在执行交互操作时，根据操作类型执行不同的查找元素方法。
你看是不是很灵活？
页面对象.元素对象.要执行的操作
如上哪个页面的哪个元素执行了什么操作清清楚楚。
"""

# 将 PageElement 构造函数参数映射到 webdriver 定位器枚举
_LOCATOR_MAP = {
    'xpath': By.XPATH,
    'id': By.ID,
    'tag_name': By.TAG_NAME,
    'name': By.NAME,
    'css': By.CSS_SELECTOR,
    'class1': By.CLASS_NAME
}


class PageElement(object):

    def __init__(self, **kwargs) -> None:
        """
        初始化方法，处理传入的元素定位键值对，并将(id='kw')其转换为(By.id, 'kw')的格式。
        实际使用如：input_account = PageElement(xpath='/html/body/section/main/div/div[2]/div/form/div[1]/div/div/input')
        """
        # 调用父类方法，初始化属性
        # super().__init__()

        # 如果没有传入关键字参数
        if not kwargs:
            # 抛出异常，提示用户需要指定一个定位器
            raise ValueError("请指定定位器")
        # 如果传入的关键字参数数量大于1
        if len(kwargs) > 1:
            # 抛出异常，提示用户只能指定一个定位器
            raise ValueError("请仅指定一个定位器")

        k, v = next(iter(kwargs.items()))  # 使用了迭代器，生成器来让(id='kw')变成By.id, 'kw'两个单独的参数

        # 检查传入的定位器键是否在_LOCATOR_MAP中
        if k not in _LOCATOR_MAP:
            raise ValueError(f"不支持的定位器类型：{k}")

        # 将定位器的键映射为对应的By类型，与值组合成元组，赋值给对象的locator属性。self.locator = (By.id, 'kw')
        self.locator = (_LOCATOR_MAP[k], v)

    def __get__(self, instance: BasePage, owner, context=None):
        """
        实现元素定位find_element()。
        获取元素，并将元素设置成全局变量
        在访问元素时，会调用该方法，保存元素对象，后续操作函数中可以直接使用。
        不获取元素，使用方法时再获取，指定查询方式。
        """
        # 如果实例对象为空，则返回None，检查是否实例化
        if instance is None:
            return None

        # 重要，将拥有该描述符的实例对象，保存在变量中，方便后续使用，
        self.instance = instance

        # 设置驱动，免得调用麻烦
        self.driver = self.instance.driver

        return self

    def __set__(self, instance, value):
        """
        实现往元素中写入内容，使用send_keys()方法。
        考虑废弃中。
        """
        # 同样的逻辑就不重复写了，直接调用__get__()方法
        self.__get__(instance, instance.__class__)
        # 然后调用封装好的输入文本方法，输入文本即可
        self.input_text(value)

    # =====================================================================================================================
    # ============================================ 元素操作方法 =============================================================
    # =====================================================================================================================

    def input_text(self, content: str or int, mode: str = "visible", alignment: bool = False,
                   move_elemnet: bool = False) -> WebElement:
        """
        输入文本内容
        :param locator: 传入元素定位表达式
        :param content: 传入输入的文本内容
        :param mode:  visible(元素可见), exist(元素存在), click(元素可点击)
        :param alignment: 默认对其方式是元素和当前页面的底部对齐，可以传 alignment=''表示和顶部对齐
        :param move_elemnet: 这里是布尔值 传入True 表示需要让元素滚动到页面可见区域 False 表示不用
        :return: 无返回值
        """
        model = self.instance.get_current_url_path()
        # 输入框使用不同的等待方式
        element = self.instance.find_element(model=model, locator=self.locator, mode=mode)
        if move_elemnet is True:
            # print("需要移动元素到页面可见区域")
            self.instance.__move_element_visible(model=model, locator=self.locator,
                                                 element=element, alignment=alignment)
        try:
            Log.debug(f"输入操作:{model}页面下的属性为:{self.locator}的元素,输入内容为{content}")
            self.driver.execute_script(
                "arguments[0].setAttribute('style', 'background: write; border: 1px solid black;');", element)
            element.send_keys(content)
        except Exception as e:
            self.instance.set_img_error()
            Log.error(f"页面{model}的属性: {self.locator} 输入操作失败")
            raise e
        return element

    def click_element(self, mode: str = "click", alignment: bool = False, move_elemnet: bool = False,
                      is_double_click: bool = False) -> None:
        """
        点击元素
        :param locator: 元素的定位表达式 例:(By.xx,'定位表达式')
        :param mode: visible(元素可见),notvisible(元素消失不可见), exist(元素存在)，click(元素可点击)
        :param alignment: 默认对其方式是元素和当前页面的底部对齐，可以传 alignment=''表示和顶部对齐
        :param move_elemnet: 这里是布尔值 传入True 表示需要让元素滚动到页面可见区域 False 表示不用
        :param is_double_click: False单击元素，传入True 双击元素
        :return: 无返回值
        """
        model = self.instance.get_current_url_path()
        element = self.instance.find_element(model=model, locator=self.locator, mode=mode)
        if move_elemnet is True:
            self.instance.__move_element_visible(model=model, locator=self.locator, element=element,
                                                 alignment=alignment)
        try:
            Log.debug(f"点击:{model}页面,属性为{self.locator}的元素")
            if is_double_click:
                ActionChains(self.driver).double_click(element).perform()
            else:
                element.click()
        except Exception as e:
            Log.error(f"页面{model}的元素: {self.locator} 点击失败")
            self.instance.set_img_error()
            raise e

    def image_ocr_identify(self, whole_name="简单验证码.png", crop_name='crop_pic.png'):
        """
        验证码图片识别
        """
        model = self.instance.get_current_url_path()
        element = self.instance.find_element(model=model, locator=self.locator)
        return image_identify(self.driver, element, whole_name, crop_name)

    def check_alert_text(self) -> None:
        # self.instance.get_current_url_path()
        # self.instance.get
        model = self.instance.get_current_url_path()
        self.instance.__wait_element_visible(model=model, locator=self.locator)
        elem_text = self.get_element_text(locator=self.locator)
        print(f"元素存在，内容是{elem_text}")
        # if elem_text == content:
        #     print()

    def set_input_files(self, content: str or int, mode: str = "exist", alignment: bool = False,
                        move_elemnet: bool = False) -> WebElement:
        """
        上传文件
        :param locator: 传入元素定位表达式
        :param content: 传入输入的文本内容
        :param mode:  visible(元素可见), exist(元素存在), click(元素可点击)
        :param alignment: 默认对其方式是元素和当前页面的底部对齐，可以传 alignment=''表示和顶部对齐
        :param move_elemnet: 这里是布尔值 传入True 表示需要让元素滚动到页面可见区域 False 表示不用
        :return: 无返回值
        """
        self.input_text(content=content, mode=mode, alignment=alignment, move_elemnet=move_elemnet)

    def clear_contents(self, locator: tuple, mode: str = "visible", alignment: bool = False,
                       move_elemnet: bool = False) -> None:
        """
        清除文本内容
        :param locator: 传入元素定位表达式
        :param mode:  visible(元素可见), exist(元素存在)
        :param alignment: 默认对其方式是元素和当前页面的底部对齐，可以传 alignment=''表示和顶部对齐
        :param move_elemnet: 这里是布尔值 传入True 表示需要让元素滚动到页面可见区域 False 表示不用
        :return: 无返回值
        """
        model = self.instance.get_current_url_path()
        element = self.instance.find_element(model=model, locator=locator, mode=mode)
        if move_elemnet is True:
            self.instance.move_element_visible(model=model, locator=locator, element=element, alignment=alignment)
        try:
            Log.debug(f"输入操作:{model}页面下的属性为:{locator}的元素,清除内容")
            # 保证正常清除
            time.sleep(0.2)
            element.clear()
        except Exception as e:
            self.instance.set_img_error()
            Log.error(f"页面{model}的属性: {locator} 清除操作失败")
            raise e

    def get_element_text(self, locator: tuple, mode: str = 'visible', alignment: bool = False,
                         move_elemnet: bool = False) -> str:
        """
        获取元素的文本内容
        :param locator:  传入元素定位表达式
        :param mode: visible(元素可见), exist(元素存在)
        :param alignment: 默认对其方式是元素和当前页面的底部对齐，可以传 alignment=''表示和顶部对齐
        :param move_elemnet: 这里是布尔值 传入True 表示需要让元素滚动到页面可见区域 False 表示不用
        :return: 返回获取到的元素文本内容
        """
        model = self.instance.get_current_url_path()
        element = self.instance.find_element(model=model, locator=locator, mode=mode)
        if move_elemnet is True:
            self.instance.move_element_visible(model=model, locator=locator, element=element, alignment=alignment)
        try:
            Log.debug(f"文本获取操作:获取{model}页面下的属性为:{locator}的元素的文本内容")
            return element.text
        except Exception as e:
            Log.error(f"页面{model}的元素:{locator}获取文本操作失败")
            self.instance.set_img_error()
            raise e

    def click_radios(self, locator: tuple, method: str, amount: int = None, mode: str = 'visible',
                     alignment: bool = False, move_elemnet: bool = False) -> WebElement:
        """
        复选框内容点击
        :param locator: 传入元素定位表达式
        :param mode:  visible(元素可见), exist(元素存在)
        :param amount: 传入复选项的数量 例子如果是3个选项就传入3
        :param method: 选择对应的内容选择方式 all 点击复选框的全部内容 random 随机点击复选框的中的某一个选项 assign点击指定的某个复选项
        :param alignment: 默认对其方式是元素和当前页面的底部对齐，可以传 alignment=''表示和顶部对齐
        :param move_elemnet: 这里是布尔值 传入True 表示需要让元素滚动到页面可见区域 False 表示不用
        :return: 全部点击时无返回，其他返回被点击的元素
        """
        # 定位到复选框一定是一组元素
        model = self.instance.get_current_url_path()
        elements: list[WebElement] = self.instance.find_elements(locator=locator, mode=mode)
        try:
            Log.debug('点击方式为：{}'.format(method))
            if method == 'all':
                # 点击复选项中每一个元素
                for ele in elements:
                    if move_elemnet is True:
                        self.instance.move_element_visible(model=model, locator=locator, element=ele,
                                                           alignment=alignment)
                    ele.click()
            # 随机点击复选项中的某一个内容
            elif method == 'random':
                # 导入随机数包
                import random
                # 生成指定范围之内的随机数作为需要点击的radio
                num = random.randint(0, amount - 1)
                element = elements[num]
                if move_elemnet is True:
                    self.instance.move_element_visible(model=model, locator=locator, element=element,
                                                       alignment=alignment)
                element.click()
                # 返回被点击的元素
                return element
            # 点击复选框中指定位置的选项
            elif method == 'assign':
                # 因为从0开始计数，所以传入的 amount-1
                element = elements[amount - 1]
                if move_elemnet is True:
                    self.instance.move_element_visible(model=model, locator=locator, element=element,
                                                       alignment=alignment)
                element.click()
                return element
            else:
                Log.error('点击方式输入错误，请检查')
        except Exception as e:
            Log.error(f"页面{model}的元素:{locator}复选框点击操作失败")
            self.instance.set_img_error()
            raise e

    def element_is_selected(self, mode: str = 'visible'):
        """
        判断元素是否勾选
        :param mode: visible(元素可见), exist(元素存在)
        :return: 选中是Ture 没有选择是False
        """
        # 获取当前页面的URL路径
        model = self.instance.get_current_url_path()

        # 需要校验是否勾选的元素。根据URL路径、定位器和模式找到需要校验是否勾选的元素
        element = self.instance.find_element(model=model, locator=self.locator, mode=mode)

        # 返回元素的勾选状态
        return element.is_selected()

    def select_contents_menu(self, text: str, mode: str = 'visible', alignment: bool = False,
                             move_element: bool = False) -> None:
        """
        选择下拉菜单中的内容
        :param text: 出入下拉列表需要选择的内容
        :param mode: visible(元素可见), exist(元素存在)
        :param alignment:  默认对其方式是元素和当前页面的底部对齐，可以传 alignment=''表示和顶部对齐
        :param move_element: 这里是布尔值 传入True 表示需要让元素滚动到页面可见区域 False 表示不用
        """
        # 获取当前页面的URL路径
        model = self.instance.get_current_url_path()
        # 传入元素定位表达式
        locator: tuple = self.locator
        # 根据URL路径、定位器和模式找到需要选择下拉列表的元素
        element = self.instance.find_element(model=model, locator=locator, mode=mode)

        # 定义一个存储菜单内容的空列表
        option = []

        # 判断是否需要移动元素到可见区域
        if move_element is True:
            # 调用滚动元素到可见区域的方法
            self.instance.move_element_visible(model=model, locator=locator, element=element, alignment=alignment)

        try:
            # 获取下拉列表的内容
            # 获取下拉列表的所有option元素
            # options = element.find_elements_by_tag_name("option")
            options = element.find_elements(by=By.TAG_NAME, value="option")
            # 遍历option元素
            for value in options:
                # 将每个option元素添加到option列表中
                option.append(value)

            print("13123", option)
            # 检查需要选择的文本是否在下拉列表中
            if text in option:
                # 通过可见文本选择下拉列表中的选项
                Select(element).select_by_visible_text(text)
            else:
                # 如果不在下拉列表中，则记录错误日志
                Log.error(f"选项:{text}不在下拉列表之中请检查")

        except Exception as e:
            # 设置图片错误
            self.instance.set_img_error()
            # 记录错误日志
            Log.error(f"页面{model}的元素:{locator}下拉框操作失败请检查")
            # 抛出异常
            raise e

    def dispose_alert(self, action: str) -> str:
        """
        处理页面alert
        :param action: 参数为 accept 点击alert的确定 dismiss点击alert的取消
        :return: 返回alert的文本内容 可能有些用例需要用这个参数去校验
        """
        # 等待alert出现，再去执行后续操作
        self.instance.wait.until(ec.alert_is_present())

        # 获取alert对象
        alert: Alert = self.instance.driver.switch_to.alert

        # 尝试点击alert
        try:
            # 判断传入的action参数是否是'accept'
            if action == 'accept':
                # 点击alert的确定按钮
                alert.accept()
            # 判断传入的action参数是否是'dismiss'
            elif action == 'dismiss':
                # 点击alert的取消按钮
                alert.dismiss()
            else:
                # 如果传入的action不是'accept'或'dismiss'，则记录错误日志
                Log.error('alert 处理参数错误请检查')

            # 获取alert的文本内容并返回
            return alert.text
        # 捕获异常
        except Exception as e:
            # 记录错误日志
            Log.error('alert处理异常')
            # 抛出异常
            raise e

    def element_whether_invisibility(self) -> bool:
        """
        判断元素是否已经消失不见了
        :param locator: 传入元素定位表达式
        :return:
        """
        # 获取当前页面的URL路径
        model = self.instance.get_current_url_path()
        locator = self.locator
        try:
            # 主要用于处理loading页面加载完成后1秒再去判断保证被判断的元素正常展示
            time.sleep(1)

            # 获取等待开始时间的时间戳
            __start_time = ReturnTime.get_Timestamp()

            # 记录日志
            Log.debug(f'判断{model}页面的元素{locator}是否消失了')

            WebDriverWait(self.driver, timeout=60, poll_frequency=0.1).until(
                ec.invisibility_of_element_located(locator))

            # 计算元素等待的时间
            __wait_time = ReturnTime.get_Timestamp() - __start_time
            # 输出调试日志，元素消失
            Log.debug(f"页面：{model}上的元素{locator}已消失，共计等待{__wait_time:0.2f}秒")
            return True
        except Exception:
            return False

    def switch_iframe(self) -> None:
        """
        判切换iframe，部分页面的元素在iframe里需要切换后定位
        :param locator: 传入元素定位表达式
        """
        # 获取当前页面的URL路径
        model = self.instance.get_current_url_path()

        try:
            # 输出调试日志，记录正在切换的iframe
            Log.debug(f'切换{model}页面的iframe{self.locator}')
            # 获取iframe元素
            element = self.instance.find_element(model=model, locator=self.locator)
            # 切换到指定的iframe
            self.driver.switch_to.frame(frame_reference=element)
        except Exception:
            # 输出错误日志，记录iframe切换失败
            Log.error(f'{model}页面的iframe切换失败')

    def mouse_over(self, times: int) -> None:
        """
        鼠标悬停功能
        :param times: 悬停N秒
        :return: None
        """
        # 获取当前页面的URL路径
        model = self.instance.get_current_url_path()
        # 根据定位表达式找到对应的元素
        element = self.instance.find_element(model=model, locator=self.locator)
        # 使用ActionChains类创建动作链，将鼠标移动到指定元素上，并执行动作
        ActionChains(self.driver).move_to_element(to_element=element).perform()
        # 悬浮times秒
        time.sleep(times)

    def select_by_visible_text(self, text, doc=""):
        """
        根据对应的下拉框的值选择对应的选项
        :param ele: 定位的元素
        :param text: 选择对应的内容选项
        :param doc: 说明文档
        :return: 无返回值
        """
        # 获取当前页面的URL路径
        model = self.instance.get_current_url_path()
        # 根据定位表达式找到对应的元素
        element = self.instance.find_element(mode="exist", model=model, locator=self.locator)

        try:
            ele_select = Select(element)
            ele_select.select_by_visible_text(text)
            Log.info(f"{doc}:{element.get_attribute('outerHTML')}选择对应的内容{text}")
        except:
            Log.error(f"操作失败:{doc}:{element.get_attribute('outerHTML')}选择对应的内容{text}")
            self.instance.set_img_error()
            raise

    def drag_and_drop(self, target: tuple = None) -> None:
        """
        拖拽功能
        :param target: 目标元素定位表达式
        :return: 无返回值
        """
        # 获取当前页面的URL路径
        model = self.instance.get_current_url_path()
        # 根据定位表达式找到对应的元素
        element = self.instance.find_element(mode="exist", model=model, locator=self.locator)
        # 根据定位表达式找到对应的元素
        target_ele = self.instance.find_element(mode="exist", model=model, locator=target)

        try:
            ActionChains(self.driver).drag_and_drop(element, target_ele).perform()
            Log.info(f"拖动{self.locator}到{target}")

        except Exception as e:
            Log.error(f"操作失败：拖动{self.locator}到{target}")
            raise e

# page_element = PageElement
# page_elements = PageElements
